/* 
 * Copyright (c) 2012, Fromentin Xavier, Schnell Michaël, Dervin Cyrielle, Brabant Quentin
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *      * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *      * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *      * The names of its contributors may not be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Fromentin Xavier, Schnell Michaël, Dervin Cyrielle OR Brabant Quentin 
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package kameleon.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.LinkedList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import kameleon.exception.FileDeletingException;
import kameleon.exception.FileReadingException;
import kameleon.exception.FileWritingException;
import kameleon.exception.InvalidPlugInException;
import kameleon.exception.KameleonException;
import kameleon.exception.UnknownPlugInException;
import kameleon.plugin.AnalyzerManager;
import kameleon.plugin.PlugIn;
import kameleon.plugin.PlugInInfo;

/**
 * Utility class for writing, reading and loading plug-ins and their files.
 * 
 * @author		Schnell Michaël
 * @version		1.0	
 */
public class IOPlugIn implements FileConstants {

	/**
	 * Utility constant for the absolute path of a jar file.
	 */
	private static final String JAR_FILE = 
			String.format("%%s%s%%s.jar", File.separator) ; //$NON-NLS-1$

	/**
	 * Manager which should be updated when a plug-in is added or removed.
	 */
	protected AnalyzerManager am ;

	/**
	 * Absolute path of the folder containing the java archives of 
	 * the analyzer plug-ins.
	 */
	protected String analyzerFolder ;

	/**
	 * Absolute path of the folder containing the java archives of 
	 * the generator plug-ins.
	 */
	protected String generatorFolder ;

	/**
	 * Absolute path of the file containing the information about
	 * the analyzer plug-ins.
	 */
	protected String analyzerConfigFile ;

	/**
	 * Absolute path of the file containing the information about
	 * the generator plug-ins.
	 */
	protected String generatorConfigFile ;

	/**
	 * Absolute path of the folder containing the resources for the plug-ins.
	 */
	protected String ressourceFolder ;

	/**
	 * Sole constructor.
	 * 
	 * @param 	am
	 * 			manager which should be updated when a plug-in is added or removed
	 * 
	 * @param 	analyzerFolder
	 * 			absolute path of the folder containing the java archives of 
	 * 			the analyzer plug-ins
	 * 
	 * @param 	generatorFolder
	 * 			absolute path of the folder containing the java archives of 
	 * 			the generator plug-ins
	 * 
	 * @param 	analyzerConfigFile
	 * 			absolute path of the file containing the information about
	 * 			the analyzer plug-ins
	 * 
	 * @param 	generatorConfigFile
	 * 			absolute path of the file containing the information about
	 * 			the generator plug-ins
	 * 
	 * @param 	ressourceFolder
	 * 			absolute path of the folder containing the resources for the plug-ins
	 */
	public IOPlugIn(AnalyzerManager am, String analyzerFolder,
			String generatorFolder, String analyzerConfigFile,
			String generatorConfigFile, String ressourceFolder) {
		super();
		this.am = am;
		this.analyzerFolder = analyzerFolder;
		this.generatorFolder = generatorFolder;
		this.analyzerConfigFile = analyzerConfigFile;
		this.generatorConfigFile = generatorConfigFile;
		this.ressourceFolder = ressourceFolder;
	}// IOPlugIn(AnalyzerManager, String, String, String, String, String)

	/**
	 * Adds a given plug-in to the software.
	 * 
	 * <p>For a plug-in to be valid, the source file should be a valid java
	 * archive. Inside this archive, there should be two things:
	 * <ol>
	 * <li>A file {@code plugin.info} containing a serialized instance of {@code PlugInInfo}.
	 * This file contains the informations about the plug-in.
	 * <li>The executable code for the plug-in (usual content of a java archive, that is {@code .class} files).
	 * </ol>
	 * 
	 * <p>The function copies the jar into the correct folder (analyzer of
	 * generator folder depending on the type of plug-in) and adds the information about
	 * the new plug-in in the concerned configuration file. If an analyzer plug-in was added,
	 * the analyzer manager is notified. Finally the pictures for the given plug-in are
	 * copied into the resource folder.
	 * 
	 * <p><b>Should any of these steps fail, the software is left in an unstable state.
	 * No actions are undone !</b>
	 * 
	 * @param 	plugin
	 * 			plug-in source file
	 * 
	 * @return	instance of {@code PlugInInfo} with the information about the newly added plug-in
	 * 
	 * @throws 	FileReadingException
	 * 			if an error occurred while reading
	 *  
	 * @throws 	FileWritingException
	 * 			if an error occurred while writing
	 * 
	 * @throws	InvalidPlugInException
	 * 			if the given plug-in is invalid
	 */
	//TODO Undo intermediate state
	public PlugInInfo addPlugIn(File plugin) 
			throws InvalidPlugInException, FileReadingException, FileWritingException {
		try {
			ZipFile plugInZip = new ZipFile(plugin) ;
			/* Retrieve plug-in information object */
			ZipEntry infoFileEntry = plugInZip.getEntry(PLUGIN_INFO_FILE_NAME) ;
			if (infoFileEntry == null) {
				throw new InvalidPlugInException(plugin.getName()) ;
			}// if
			PlugInInfo info = (PlugInInfo) IOObject.readObjectFromFile(
					new BufferedInputStream(
							plugInZip.getInputStream(infoFileEntry))) ;

			// Close the zip file in order to copy it
			plugInZip.close() ;

			/* Determine the nature of the plug-in before adding it */
			if (info.isAnalyzer()) {
				addJar(plugin, info, this.analyzerFolder) ;
				addConfiguration(info, this.analyzerConfigFile) ;
				this.am.addAnalyzerInfo(info) ;
			} else {
				addJar(plugin, info, this.generatorFolder) ;
				addConfiguration(info, this.generatorConfigFile) ;
			}// if
			copyImages(info) ;
			
			return info ;
		} catch (ZipException e) {
			throw new InvalidPlugInException(plugin.getName()) ;
		} catch (IOException e) {
			throw new FileReadingException(plugin) ;
		}// try
	}// addPlugIn(File, String, String, String)

	/**
	 * Copies the given zip file into the given folder.
	 * 
	 * @param 	pluginFile
	 * 			source zip file
	 * 
	 * @param 	info
	 * 			instance of {@code PlugInInfo} for the given plug-in
	 * 
	 * @param 	tagetFolder
	 * 			target folder for the extracted jar
	 *  
	 * @throws 	FileReadingException
	 * 			if an error occurred while reading
	 *  
	 * @throws 	FileWritingException
	 * 			if an error occurred while writing
	 */
	private static void addJar(File pluginFile, PlugInInfo info, String tagetFolder) 
			throws FileReadingException, FileWritingException {
		String jarName = String.format("%s.jar", info.getJarName()) ; //$NON-NLS-1$
		
		/* Create the streams */
		InputStream src ;
		try {
			src = new BufferedInputStream(
					new FileInputStream(pluginFile)) ;
		} catch (IOException e) {
			throw new FileReadingException() ;
		}// try
		OutputStream dest ;
		try {
			dest = new BufferedOutputStream(
					new FileOutputStream(String.format(PATH_EXTENSION,
							tagetFolder, jarName))) ;
		} catch (IOException e) {
			throw new FileWritingException() ;
		}// try

		/* Do the actual copying */
		IOFile.copyFile(src, dest) ;

		/* Close the streams */
		try {
			src.close() ;
		} catch (IOException e) {
			throw new FileReadingException() ;
		}// try
		try {
			dest.close() ;
		} catch (IOException e) {
			throw new FileWritingException() ;
		}// try
	}// addJar(ZipFile, PlugInInfo, String)

	/**
	 * Adds the given {@code PlugInInfo} to the list read in the given 
	 * configuration file.
	 * 
	 * @param 	info
	 * 			instance to add in the list
	 * 
	 * @param 	configFile
	 * 			source configuration file for the list
	 *  
	 * @throws 	FileWritingException
	 * 			if an error occurred while writing
	 */
	private static void addConfiguration(PlugInInfo info, String configFile) throws FileWritingException 
	{
		List<PlugInInfo> knownPlugIns = null ;
		try {
			knownPlugIns = IOObject.readList(configFile) ;
		} catch (KameleonException e) {
			knownPlugIns = new LinkedList<PlugInInfo>() ;
		}// try

		int index = knownPlugIns.indexOf(info) ;
		// Plug-in is new
		if (index == -1) {
			knownPlugIns.add(info) ;
		} else {// Plug-in is being updated
			knownPlugIns.set(index, info) ;
		}// if

		IOObject.writeObjectToFile(configFile, knownPlugIns) ;
	}// addConfiguration(PlugInInfo, String)

	/**
	 * Removes the given plug-in from the software.
	 * 
	 * @param	info
	 * 			information about the plug-in which should be removed
	 * 
//	 * @throws 	FileDeletingException
//	 * 			if files used by the plug-in could not be deleted
	 * 
	 * @throws	UnknownPlugInException 
	 * 			if the removed plug-in does not exist
	 */
	//TODO Undo intermediate state
	public void removePlugIn(PlugInInfo info) 
			throws /*FileDeletingException, */UnknownPlugInException {
		String configFile, folder ;
		if (info.isAnalyzer()) {
			configFile = this.analyzerConfigFile ;
			folder = this.analyzerFolder ;
		} else {
			configFile = this.generatorConfigFile ;
			folder = this.generatorFolder ;
		}// if
		removeConfiguration(info, configFile) ;
		try {
			removeJar(folder, info.getJarName()) ;
			//TODO Handle the case of partially removed pictures
			removeImages(info) ;
		} catch (KameleonException ke) {
			// TODO: handle exception
			forceRemoveImages(info) ;
		}// try
	}// removePlugIn(PlugInInfo)

	/**
	 * Deleted the given jar file from the given folder.
	 * 
	 * @param 	parentFolder
	 * 			folder containing the jar file which should be deleted
	 * 
	 * @param 	jarName
	 * 			name of the jar file which should be deleted (without the extension)
	 * 
	 * @throws 	FileDeletingException
	 * 			if the jar of the plug-in could not be deleted
	 */
	private static void removeJar(String parentFolder, String jarName) 
			throws FileDeletingException {
		String jarPath = String.format(JAR_FILE, parentFolder, jarName) ;
		File jar = new File(jarPath) ;
		boolean sucess = jar.delete() ;
		if (!sucess) {
			throw new FileDeletingException(jar) ;
		}// if
	}// removeJar(String)

	/**
	 * Removes the given instance of {@code PlugInInfo} from the list read
	 * in the given configuration file. The configuration file is updated 
	 * after the removal.
	 * 
	 * @param 	info
	 * 			instance of [@ode PlugInInfo] which should be removed
	 * 
	 * @param 	configFile
	 * 			source configuration file for the list from which the
	 * 			instance should be removed
	 * 
	 * @throws	UnknownPlugInException
	 * 			if the given plug-in is not listed in the configuration file
	 *///TODO Review thrown exceptions
	private static void removeConfiguration(PlugInInfo info, String configFile) 
			throws UnknownPlugInException {
		try {
			List<PlugInInfo> knownPlugIns = IOObject.readList(configFile) ;

			// Plug-in is unknown
			if (!knownPlugIns.contains(info)) {
				throw new UnknownPlugInException(info) ;
			}// if 

			// Remove plug-in info and save the result
			knownPlugIns.remove(info) ;
			IOObject.writeObjectToFile(configFile, knownPlugIns) ;
		} catch(UnknownPlugInException uke) {
			throw uke ;
		} catch(KameleonException ke) {
			/* Configuration file is corrupt, we cannot remove the plug-in. */
		}// try
	}// removeConfiguration(PlugInInfo, String)

	/**
	 * Removes the pictures used by the given plug-in.
	 * 
	 * @param	info
	 * 			information about the plug-in whose file are removed
	 * 
	 * @throws	FileDeletingException 
	 * 			if a picture could not be deleted
	 */
	private static void removeImages(PlugInInfo info)
		throws FileDeletingException {
		String basePath = String.format(PATH_EXTENSION,
				PLUG_IN_RESOURCES_FOLDER, info.getId()) ;
		String[] pictures = new String[]{
				FORMAT_GRAY_ICON_FILE_NAME,
				FORMAT_ICON_FILE_NAME,
				FORMAT_MINI_FILE_NAME
		} ;
		for(String picture : pictures) {
			File pictureFile = new File(
					String.format(picture, basePath)) ;
			boolean deleted ;
			try {
				deleted = pictureFile.delete() ;
			} catch (SecurityException se) {
				deleted = false;
			}// try
			
			if (!deleted) {
				throw new FileDeletingException(pictureFile) ;
			}// if
		}// for
	}// removeImages(PlugInInfo)

	/**
	 * Removes the pictures used by the given plug-in without throwing
	 * an exception if one file could not be deleted.
	 * 
	 * @param	info
	 * 			information about the plug-in whose file are removed
	 */
	private static void forceRemoveImages(PlugInInfo info) {
		String basePath = String.format(PATH_EXTENSION,
				PLUG_IN_RESOURCES_FOLDER, info.getId()) ;
		String[] pictures = new String[]{
				FORMAT_GRAY_ICON_FILE_NAME,
				FORMAT_ICON_FILE_NAME,
				FORMAT_MINI_FILE_NAME
		} ;
		for(String picture : pictures) {
			File pictureFile = new File(
					String.format(picture, basePath)) ;
			try {
				pictureFile.delete() ;
			} catch (SecurityException se) {
				/* Ignore exception. */
			}// try
		}// for
	}// removeImages(PlugInInfo)
	
	/**
	 * Copies the pictures of the plug-in into the resource folder.
	 * 
	 * <p>The copying is achieved by calling the {@code copyPicture(String)} function
	 * from the "main" class of the given plug-in.
	 * 
	 * @param 	info
	 * 			information about the plug-in whose pictures should be copied
	 * 
	 * @throws	InvalidPlugInException
	 * 			if the given plug-in is invalid
	 * 
	 * @see		PlugInInfo
	 * @see		PlugIn#copyPicture(String)
	 */
	private void copyImages(PlugInInfo info) throws InvalidPlugInException {
		ClassLoader loader = null ;
		if (info.isAnalyzer()) {
			loader = loadPlugIn(info, this.analyzerFolder) ;
		} else {
			loader = loadPlugIn(info, this.generatorFolder) ;
		}// if
		try {
			loader.loadClass(info.getPlugInClass()) ;
			Class<?> plugInClass = Class.forName(info.getPlugInClass(), true, loader) ;
			Class<? extends PlugIn> plClass = plugInClass.asSubclass(PlugIn.class) ;
			Constructor<? extends PlugIn> constructor = plClass.getConstructor() ;
			PlugIn plugIn = constructor.newInstance() ;
			plugIn.copyPicture(this.ressourceFolder) ;
		} catch (Exception e) {
			/*=== Possible exceptions ===
			 * - ClassNotFoundException
			 * - IllegalAccessException
			 * - IllegalArgumentException
			 * - InstantiationException
			 * - InvocationTargetException
			 * - NoSuchMethodException
			 * - SecurityException
			 */
			throw new InvalidPlugInException(info, e) ;
		}// try
	}// copyImages(ZipFile)

	/**
	 * Returns an instance of {@code ClassLoader} containing the classes of the
	 * loaded plug-in.
	 * 
	 * @param 	info
	 * 			information about the loaded plug-in
	 * 
	 * @param 	parentFolder
	 * 			folder containing the java archive of the loaded plug-in
	 * 
	 * @return	A {@code ClassLoader} which can be used to instantiate the "main"
	 * 			class from the loaded plug-in
	 * 
	 * @throws	InvalidPlugInException
	 * 			if the given plug-in is invalid
	 */
	public static ClassLoader loadPlugIn(PlugInInfo info, String parentFolder) 
			throws InvalidPlugInException {
		String jarPath = String.format(JAR_FILE, parentFolder, info.getJarName()) ;
		try {
			ClassLoader loader = URLClassLoader.newInstance(
					new URL[] { new File(jarPath).toURI().toURL() }
					) ;
			return loader ;
		} catch (MalformedURLException e) {
			throw new InvalidPlugInException(info) ;
		}// try
	}// loadPlugIn(PlugInInfo, String, String)

}// class IOPlugIn